SPB Git
2.8 KB · 27 lines typescript
1import { createReadStream, promises as fs } from "node:fs";2import { Readable } from "node:stream";3import { publicFile } from "@/lib/papers/queries";4import { recordEvent } from "@/lib/papers/events";5import { storagePath } from "@/lib/pdf/process";6import { ip, limited, tooMany } from "@/lib/api";7export const dynamic = "force-dynamic";8/** Serves a published manuscript version (content-addressed on disk). Counts one download per visitor per paper per day. Supports Range and HEAD. */9export async function GET(req: Request, ctx: { params: Promise<{ fileId: string; name: string }> }) { return serve(req, ctx, false); }10export async function HEAD(req: Request, ctx: { params: Promise<{ fileId: string; name: string }> }) { return serve(req, ctx, true); }11async function serve(req: Request, { params }: { params: Promise<{ fileId: string; name: string }> }, head: boolean) {12  if (limited(req, "files", 60)) return tooMany();13  const { fileId, name } = await params; if (!/^[0-9a-f-]{36}$/.test(fileId)) return new Response("Not found", { status: 404 });14  const f = await publicFile(fileId); if (!f) return new Response("Not found", { status: 404 });15  const path = storagePath(f.storage_key); let st; try { st = await fs.stat(path); } catch { return new Response("Missing file", { status: 404 }); }16  const inline = new URL(req.url).searchParams.get("inline") === "1";17  const filename = name.endsWith(".pdf") ? name.replace(/[^A-Za-z0-9._-]/g, "_") : `${f.slug}-${f.label}.pdf`;18  const headers: Record<string, string> = { "Content-Type": "application/pdf", "Content-Disposition": `${inline ? "inline" : "attachment"}; filename="${filename}"`, "Accept-Ranges": "bytes", "Cache-Control": "public, max-age=86400, immutable", ETag: `"${f.sha256}"`, "X-Content-Type-Options": "nosniff", "Content-Security-Policy": "sandbox", "X-Checksum-SHA256": f.sha256 };19  if (req.headers.get("if-none-match") === `"${f.sha256}"`) return new Response(null, { status: 304, headers });20  if (!head && !inline) recordEvent("download", f.paper_id, { ip: ip(req), ua: req.headers.get("user-agent"), referrer: req.headers.get("referer"), versionId: f.version_id }).catch(() => {});21  const range = req.headers.get("range"); let start = 0, end = st.size - 1, status = 200;22  if (range) { const m = /bytes=(\d*)-(\d*)/.exec(range); if (m) { if (m[1]) start = Number(m[1]); if (m[2]) end = Number(m[2]); if (!m[1] && m[2]) { start = st.size - Number(m[2]); end = st.size - 1; } if (start > end || end >= st.size) return new Response(null, { status: 416, headers: { "Content-Range": `bytes */${st.size}` } }); status = 206; headers["Content-Range"] = `bytes ${start}-${end}/${st.size}`; } }23  headers["Content-Length"] = String(end - start + 1);24  if (head) return new Response(null, { status, headers });25  return new Response(Readable.toWeb(createReadStream(path, { start, end })) as unknown as ReadableStream, { status, headers });26}27
Files · main
Menu

Theme